<html>
    <head>
    </head>
    <body>
        This is a test of the Web Audio API
        
        <div id="Error" style="color:red">
        </div>
        
        <div><b>Playback Devices</b></div>
        <div id="PlaybackDevices">
            [Playback Devices]
        </div>
        
        <div><b>Capture Devices</b></div>
        <div id="CaptureDevices">
            [Capture Devices]
        </div>
        
        <button id="btnStartPlayback">
            Start Playback
        </button>
        <button id="btnStopPlayback">
            Stop Playback
        </button>
        <button id="btnClosePlayback">
            Close Playback
        </button>
    
        <br/>
    
        <button id="btnStartCapture">
            Start Capture
        </button>
        <button id="btnStopCapture">
            Stop Capture
        </button>
        <button id="btnCloseCapture">
            Close Capture
        </button>
    
        <script>
            var runningTime = 0.0;
        
            function ma_enum_devices(deviceType) {
                if (deviceType !== 'audiooutput' && deviceType !== 'audioinput') {
                    alert("Invalid device type: " + deviceType);
                    return null;
                }
                
                // Unfortunately the navigator.mediaDevices.enumerateDevices() API doesn't seem to be very well supported.
                //   1) On Chrome and Opera the label is always an empty string
                //   2) No devices are returned in Firefox.
                //
                // This is not a production quality solution right now. It's better to instead just assume default
                // devices and output/input silence if they fail to open (if a microphone is not connected, for example).
                var promise = new Promise(function(resolve, reject) {
                    var devices = [];
                    navigator.mediaDevices.enumerateDevices().then(function(deviceInfos) {
                        for (var i = 0; i < deviceInfos.length; ++i) {
                            if (deviceInfos[i].kind === deviceType) {
                                devices.push(deviceInfos[i]);
                            }
                        }
                        resolve(devices);
                    }).catch(function(error) {
                        reject("Failed to enumerate devices: " + error);
                    });
                });
                
                return promise;
            }
        
            function ma_device_new(deviceType, deviceID) {
                if (typeof(mal) === 'undefined') {
                    return null;   // Context not initialized.
                }
                
                // For now only default devices are being used. The device ID needs to be null.
                if (deviceID != null) {
                    return null;    // Only default devices are currently supported.
                }
                
                if (deviceID == null) {
                    deviceID = "";
                }
                
                var bufferSizeInFrames = 512;
                var sampleRate = 44100;
                var channelCount = 2;
                
                var device = {};
                
                device.webaudioContext = new (window.AudioContext || window.webkitAudioContext)({
                    latencyHint: 'interactive',
                    sampleRate: sampleRate,
                });
                device.webaudioContext.suspend();  // miniaudio always starts it's devices in a stopped state.
                console.log("Sample Rate: " + device.webaudioContext.sampleRate);
                
                device.intermediaryBufferSizeInBytes = channelCount * bufferSizeInFrames * 4;
                //device.intermediaryBuffer = Module._malloc(device.intermediaryBufferSizeInBytes);
                device.intermediaryBuffer = new Float32Array(channelCount * bufferSizeInFrames);
                
                if (deviceType == 'audiooutput') {
                    device.playback = {};
                    device.playback.scriptNode = device.webaudioContext.createScriptProcessor(
                        bufferSizeInFrames,
                        channelCount,
                        channelCount
                    );
                    device.playback.scriptNode.onaudioprocess = function(e) {
                        // TODO: Don't do anything if we don't have an intermediary buffer. This means the device
                        // was uninitialized.
                    
                        // The buffer we give to the client needs to be interleaved. After the client callback has returned
                        // we deinterleave it.
                        var requiredBufferLength = channelCount * e.outputBuffer.length;
                        if (device.intermediaryBuffer.length < requiredBufferLength) {
                            device.intermediaryBuffer = new Float32Array(requiredBufferLength);
                        }
                        
                        // Here is where we get the client to fill the buffer with audio data.
                        
                        // TESTING: Output a sine wave to the speakers.
                        for (var iFrame = 0; iFrame < e.outputBuffer.length; ++iFrame) {
                            var value = Math.sin((runningTime+(iFrame*6.28318530717958647693/44100.0)) * 400.0) * 0.25;
                            for (var iChannel = 0; iChannel < channelCount; ++iChannel) {
                                device.intermediaryBuffer[iFrame*channelCount + iChannel] = value;
                            }
                        }
                        runningTime += (6.28318530717958647693*e.outputBuffer.length) / 44100.0;
                        
                        // At this point the intermediary buffer should be filled with data. We now need to deinterleave
                        // it and write it to the output buffer.
                        for (var iChannel = 0; iChannel < channelCount; ++iChannel) {
                            for (var iFrame = 0; iFrame < e.outputBuffer.length; ++iFrame) {
                                e.outputBuffer.getChannelData(iChannel)[iFrame] = device.intermediaryBuffer[iFrame*channelCount + iChannel];
                            }
                        }
                    };
                    device.playback.scriptNode.connect(device.webaudioContext.destination);
                } else if (deviceType == 'audioinput') {
                    device.capture = {};

                    navigator.mediaDevices.getUserMedia({audio:true, video:false})
                        .then(function(stream) {
                            // We need to use ScriptProcessorNode instead of MediaRecorder because we need raw PCM data
                            // rather than compressed data. Why is this not supported? Seriously...
                            //
                            // This way this works is that we connect the output of a MediaStreamSourceNode to the input
                            // of a ScriptProcessorNode. The ScriptProcessorNode is connected to the AudioContext
                            // destination, but instead of connecting the input to the output we just output silence.
                            device.capture.streamNode = device.webaudioContext.createMediaStreamSource(stream);
                            device.capture.scriptNode = device.webaudioContext.createScriptProcessor(
                                bufferSizeInFrames,
                                channelCount,
                                channelCount
                            );
                            device.capture.scriptNode.onaudioprocess = function(e) {
                                // The input buffer needs to be interleaved before sending to the client. We need to do
                                // this in an intermediary buffer.
                                var requiredBufferLength = e.inputBuffer.numberOfChannels * e.inputBuffer.length;
                                if (device.intermediaryBuffer.length < requiredBufferLength) {
                                    device.intermediaryBuffer = new Float32Array(requiredBufferLength);
                                }
                                
                                for (var iFrame = 0; iFrame < e.inputBuffer.length; ++iFrame) {
                                    for (var iChannel = 0; iChannel < e.inputBuffer.numberOfChannels; ++iChannel) {
                                        device.intermediaryBuffer[iFrame*e.inputBuffer.numberOfChannels + iChannel] = e.inputBuffer.getChannelData(iChannel)[iFrame];
                                    }
                                }
                                
                                // At this point the input data has been interleaved and can be passed on to the client.
                                
                                
                                // Always output silence.
                                for (var iChannel = 0; iChannel < e.outputBuffer.numberOfChannels; ++iChannel) {
                                    e.outputBuffer.getChannelData(iChannel).fill(0.0);
                                }
                                
                                /*
                                // TESTING: Write to the interleaved data to the output buffers.
                                for (var iChannel = 0; iChannel < e.inputBuffer.numberOfChannels; ++iChannel) {
                                    for (var iFrame = 0; iFrame < e.inputBuffer.length; ++iFrame) {
                                        e.outputBuffer.getChannelData(iChannel)[iFrame] = device.intermediaryBuffer[iFrame*e.inputBuffer.numberOfChannels + iChannel];
                                    }
                                }
                                */
                            };
                            device.capture.streamNode.connect(device.capture.scriptNode);
                            device.capture.scriptNode.connect(device.webaudioContext.destination);
                        })
                        .catch(function(error) {
                            // For now just do nothing, but later on we may want to periodically fire the callback with silence.
                            console.log("No Stream.");
                        });
                } else {
                    return null;    // Unknown device type.
                }
                
                return device;
            }
            
            function ma_device_delete(device) {
                Module._free(device.intermediaryBuffer);
            }
        
            function ma_context_init() {
                if ((window.AudioContext || window.webkitAudioContext) === undefined) {
                    return 0;   // Web Audio not supported.
                }
                if (typeof(Float32Array) === 'undefined') {
                    return 0;   // Float32Array not supported.
                }
                
            
                if (typeof(mal) === 'undefined') {
                    mal = {};
                    miniaudio.devices = [];   // Device cache for mapping devices to indexes for JavaScript/C interop.
                    
                    // Returns the index of the device. Throws an exception on error.
                    miniaudio.track_device = function(device) {
                        // Try inserting into a free slot first.
                        for (var iDevice = 0; iDevice < miniaudio.devices.length; ++iDevice) {
                            if (miniaudio.devices[iDevice] == null) {
                                miniaudio.devices[iDevice] = device;
                                return iDevice;
                            }
                        }
                        
                        // Getting here means there is no empty slots in the array so we just push to the end.
                        miniaudio.devices.push(device);
                        return miniaudio.devices.length - 1;
                    };
                    
                    miniaudio.untrack_device_by_index = function(deviceIndex) {
                        // We just set the device's slot to null. The slot will get reused in the next call to ma_track_device.
                        miniaudio.devices[iDevice] = null;
                        
                        // Trim the array if possible.
                        while (miniaudio.devices.length > 0) {
                            if (miniaudio.devices[miniaudio.devices.length-1] == null) {
                                miniaudio.devices.pop();
                            } else {
                                break;
                            }
                        }
                    };
                    
                    miniaudio.untrack_device = function(device) {
                        for (var iDevice = 0; iDevice < miniaudio.devices.length; ++iDevice) {
                            if (miniaudio.devices[iDevice] == device) {
                                return miniaudio.untrack_device_by_index(iDevice);
                            }
                        }
                    };
                    
                    miniaudio.get_device_by_index = function(deviceIndex) {
                        return miniaudio.devices[deviceIndex];
                    };
                }
                
                return 1;
            }
        
            window.onload = function() {
                if (ma_context_init() != 1) {
                    alert("Failed to initialize context.");
                    return;
                }
                

                // Unfortunately this doesn't seem to work too well. See comment in ma_enum_devices().
                ma_enum_devices('audiooutput').then(function(outputDevices) {
                    for (var iDevice = 0; iDevice < outputDevices.length; ++iDevice) {
                        console.log("Output Device: ", JSON.stringify(outputDevices[iDevice]));
                    }
                }).catch(function(error) {
                    console.log("Failed to retrieve output devices: ", error);
                });
                
                ma_enum_devices('audioinput').then(function(inputDevices) {
                    for (var iDevice = 0; iDevice < inputDevices.length; ++iDevice) {
                        console.log("Input Device: ", JSON.stringify(inputDevices[iDevice]));
                    }
                }).catch(function(error) {
                    console.log("Failed to retrieve input devices: ", error);
                });
                
                
                var outputDevice = ma_device_new('audiooutput', null);
                var inputDevice = ma_device_new('audioinput', null);

                var btnStartPlayback = document.getElementById("btnStartPlayback");
                btnStartPlayback.addEventListener('click', function() {
                    outputDevice.webaudioContext.resume();
                });
                
                var btnStopPlayback = document.getElementById("btnStopPlayback");
                btnStopPlayback.addEventListener('click', function() {
                    outputDevice.webaudioContext.suspend();
                });
                
                var btnClosePlayback = document.getElementById("btnClosePlayback");
                btnClosePlayback.addEventListener('click', function() {
                    outputDevice.webaudioContext.close();
                });
                
                
                var btnStartCapture = document.getElementById("btnStartCapture");
                btnStartCapture.addEventListener('click', function() {
                    inputDevice.webaudioContext.resume();
                });
                
                var btnStopCapture = document.getElementById("btnStopCapture");
                btnStopCapture.addEventListener('click', function() {
                    inputDevice.webaudioContext.suspend();
                });
                
                var btnCloseCapture = document.getElementById("btnCloseCapture");
                btnCloseCapture.addEventListener('click', function() {
                    inputDevice.webaudioContext.close();
                });
            }
        </script>
        
        
    </body>
</html>